Since 2015, JavaScript has improved immensely.
It’s much more pleasant to use it now than ever.
In this article, we’ll look at JavaScript iterable objects.
Closable Iterators
An iterator is closable if it has the return
method.
This is an optional feature.
Array iterators aren’t closable.
For instance, if we write:
const arr = ['foo', 'bar', 'baz'];
const iterator = arr[Symbol.iterator]();
console.log('return' in iterator)
We see that there’s no return
method.
On the other hand, generator objects are closable by default.
For instance, if we have:
function* genFn() {
yield 'foo';
yield 'bar';
yield 'baz';
}
const gen = genFn();
We can finish calling the generator gen
by calling the return
method on it.
For instance, we can write:
function* genFn() {
yield 'foo';
yield 'bar';
yield 'baz';
}
const gen = genFn();
console.log(gen.next());
gen.return();
console.log(gen.next());
console.log(gen.next());
After we called return
, then the generator is done.
So the last 2 return
calls give us:
{value: undefined, done: true}
If an iterator isn’t closable, we can continue iterating over it after an exit from a for-of loop.
So if we have an iterable with a non-closable iterator like an array, we can continue looping through it:
const arr = ['foo', 'bar', 'baz'];
for (const x of arr) {
console.log(x);
break;
}
for (const x of arr) {
console.log(x);
}
After we used break
to break out of the first loop, the 2nd loop still iterate from start to end.
Prevent Iterators from Being Closed
We can prevent iterators from being closed by changing the return
method.
If we return done
set to false
, then it can’t be closed.
For example, we can write:
class NoReturn {
constructor(iterator) {
this.iterator = iterator;
}
[Symbol.iterator]() {
return this;
}
next() {
return this.iterator.next();
}
return (value) {
return {
done: false,
value
};
}
}
function* genFn() {
yield 'foo';
yield 'bar';
yield 'baz';
}
const gen = genFn();
const noReturn = new NoReturn(gen);
for (const x of noReturn) {
console.log(x);
break;
}
for (const x of noReturn) {
console.log(x);
}
Since we passed our generator, which is closeable, to the NoReturn
constructor, it’ll continue iterating through the items.
So we get:
foo
bar
baz
logged.
It just continues the iteration from where it left off since it hasn’t been closed.
So it can continue iteration from where it left off.
We can also make generators unclosable by setting the prototype.return
method to undefined
.
However, we should be aware that this doesn’t work with all transpilers.
For instance, we can write:
function* genFn() {
yield 'foo';
yield 'bar';
yield 'baz';
}
genFn.prototype.return = undefined;
We set the genFn.prototype.return
property to be undefined
.
Then we can confirm that it’s unclosable by writing:
const gen = genFn();
for (const x of gen) {
console.log(x);
break;
}
for (const x of gen) {
console.log(x);
}
We get:
foo
bar
baz
from the 2 loops.
Using break
didn’t close the generator.
Conclusion
We can adjust whether we can close an iterator.
If closing is disabled, this will let us loop through the iterator items after breaking and similar operations.